package cn.redragon.soa.common.util;


import cn.redragon.soa.common.constant.Constants;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Objects;
import java.util.TimeZone;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.util.Assert;

/**
 * Date util for converting String/Date/LocalDateTime
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DateUtil {

    public static Date getLastTimeOfDate(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.HOUR, 23);
        calendar.set(Calendar.MINUTE, 59);
        calendar.set(Calendar.SECOND, 59);
        return calendar.getTime();
    }

    /**
     * convert {@link LocalDateTime} to {@link Date}
     */
    public static Date localDateTimeToDate(LocalDateTime localDateTime) {
        if (localDateTime == null) {
            return null;
        }
        ZoneId zone = ZoneId.systemDefault();
        Instant instant = localDateTime.atZone(zone).toInstant();

        return Date.from(instant);
    }

    /**
     * convert {@link LocalDate} to {@link Date}
     */
    public static Date localDateToDate(LocalDate localDate) {
        if (localDate == null) {
            return null;
        }
        return localDateTimeToDate(localDate.atStartOfDay());
    }

    /**
     * convert {@link Date} to {@link LocalDateTime}
     */
    public static LocalDateTime dateToLocalDateTime(Date date) {
        if (date == null) {
            return null;
        }
        return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    }

    // Conversion from LocalDate to String

    /**
     * convert {@link LocalDate} to {@link String} with default pattern 'yyyy-MM-dd'
     */
    public static String format(LocalDate localDate) {
        if (localDate == null) {
            return null;
        }
        return format(localDate, Constants.DateTimePattern.FMT_DATE);
    }

    /**
     * convert {@link LocalDate} to {@link String}
     */
    public static String format(LocalDate localDate, String pattern) {
        if (localDate == null) {
            return null;
        }
        return DateTimeFormatter.ofPattern(pattern).format(localDate);
    }

    /**
     * convert {@link LocalDateTime} to {@link String} with default pattern 'yyyy-MM-dd HH:mm:ss'
     */
    public static String format(LocalDateTime localDateTime) {
        if (localDateTime == null) {
            return null;
        }
        return format(localDateTime, Constants.DateTimePattern.FMT_DATE_TIME);
    }

    /**
     * convert {@link LocalDateTime} to {@link String} with pattern
     */
    public static String format(LocalDateTime localDateTime, String pattern) {
        if (localDateTime == null) {
            return null;
        }
        return DateTimeFormatter.ofPattern(pattern).format(localDateTime);
    }

    /**
     * convert {@link LocalTime} to {@link String} with default pattern 'HH:mm'
     */
    public static String format(LocalTime localTime) {
        if (localTime == null) {
            return null;
        }
        return format(localTime, Constants.DateTimePattern.FMT_TIME);
    }

    /**
     * convert {@link LocalTime} to {@link String} with pattern
     */
    public static String format(LocalTime time, String pattern) {
        if (time == null) {
            return null;
        }
        return DateTimeFormatter.ofPattern(pattern).format(time);
    }

    // Conversion from String to LocalDate

    /**
     * convert {@link String} to {@link LocalDateTime} with default pattern 'yyyy-MM-dd HH:mm:ss'
     */
    public static LocalDateTime parseToLocalDateTime(String timeStr) {
        return parseToLocalDateTime(timeStr, Constants.DateTimePattern.FMT_DATE_TIME);
    }

    /**
     * convert {@link String} to {@link LocalDateTime} with pattern
     */
    public static LocalDateTime parseToLocalDateTime(String timeStr, String pattern) {
        return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * convert {@link String} to {@link LocalDate} with default pattern 'yyyy-MM-dd'
     */
    public static LocalDate parseToLocalDate(String timeStr) {
        return parseToLocalDate(timeStr, Constants.DateTimePattern.FMT_DATE);
    }

    /**
     * convert {@link String} to {@link LocalDate} with pattern
     */
    public static LocalDate parseToLocalDate(String timeStr, String pattern) {
        return LocalDate.parse(timeStr, DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * convert {@link String} to {@link LocalTime} with default pattern 'HH:mm'
     */
    public static LocalTime parseToLocalTime(String timeStr) {
        return parseToLocalTime(timeStr, Constants.DateTimePattern.FMT_TIME);
    }

    /**
     * convert {@link String} to {@link LocalTime} with pattern
     */
    public static LocalTime parseToLocalTime(String timeStr, String pattern) {
        return LocalTime.parse(timeStr, DateTimeFormatter.ofPattern(pattern));
    }

    // Conversion between Date and String

    /**
     * convert {@link Date} to {@link String} with default pattern 'yyyy-MM-dd HH:mm:ss'
     */
    public static String format(Date date) {
        if (date == null) {
            return null;
        }
        return format(date, Constants.DateTimePattern.FMT_DATE_TIME);
    }

    /**
     * convert {@link Date} to {@link String} with pattern
     */
    public static String format(Date date, String pattern) {
        if (date == null) {
            return null;
        }
        return format(dateToLocalDateTime(date), pattern);
    }

    /**
     * convert {@link String} to {@link Date} with default pattern 'yyyy-MM-dd HH:mm:ss'
     */
    public static Date parseToDate(String timeStr) {
        return parseToDate(timeStr, Constants.DateTimePattern.FMT_DATE_TIME);
    }

    /**
     * convert {@link String} to {@link Date} with pattern
     */
    public static Date parseToDate(String timeStr, String pattern) {
        return localDateTimeToDate(parseToLocalDateTime(timeStr, pattern));
    }

    public static XMLGregorianCalendar convertToXMLGregorianCalendar(Date date) {
        if (date == null) {
            return null;
        }
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(date);
        XMLGregorianCalendar gc = null;
        try {
            gc = DatatypeFactory.newInstance().newXMLGregorianCalendar(cal);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return gc;
    }

    public static String convertXMLGregorianCalendarToDateStr(XMLGregorianCalendar cal, String pattern) {
        if (cal == null) {
            return null;
        }
        GregorianCalendar ca = cal.toGregorianCalendar();
        return format(ca.getTime(), pattern);
    }

    public static LocalDate convertXMLGregorianCalendarToLocalDate(XMLGregorianCalendar cal) {
        if (cal == null) {
            return null;
        }
        GregorianCalendar ca = cal.toGregorianCalendar();
        return dateToLocalDateTime(ca.getTime()).toLocalDate();
    }

    public static Date getDateAfter(Date date, int days) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.DATE, calendar.get(Calendar.DATE) + days);

        return calendar.getTime();
    }

    /**
     * Convert unix epoch timestamp to java LocalDateTime
     */
    public static LocalDateTime epochSecond2LocalDateTime(Long timestamp) {
        Objects.requireNonNull(timestamp, "Input timestamp is null!");
        return ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), TimeZone.getDefault().toZoneId()).toLocalDateTime();
    }

    /**
     * Convert java LocalDateTime to unix epoch timestamp.
     */
    public static Long localDateTime2EpochSecond(LocalDateTime dateTime) {
        Objects.requireNonNull(dateTime, "Input dateTime is null!");
        return dateTime.atZone(ZoneId.systemDefault()).toEpochSecond();
    }

    /**
     * Left: including,
     * Right: excluding.
     */
    public static Pair<LocalDateTime, LocalDateTime> getYesterdayRange() {
        LocalDateTime now = LocalDateTime.now(), yesterday = now.plusDays(-1);
        LocalDateTime start = LocalDateTime.of(yesterday.getYear(), yesterday.getMonth(), yesterday.getDayOfMonth(), 0, 0, 0);
        LocalDateTime end = LocalDateTime.of(now.getYear(), now.getMonth(), now.getDayOfMonth(), 0, 0, 0);
        return Pair.of(start, end);
    }

    /**
     * Current time is between beginTime and endTime.
     * @param beginTime
     * @param endTime
     * @return
     */
    public static boolean duringPeriod(LocalDateTime beginTime, LocalDateTime endTime) {
        return duringPeriod(beginTime, endTime, LocalDateTime.now());
    }

    public static boolean duringPeriod(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime currentTime) {
        if (beginTime == null && endTime == null) {
            return true;
        }
        if (beginTime == null) {
            return currentTime.isBefore(endTime);
        } else {
            return currentTime.isAfter(beginTime) && (endTime == null || currentTime.isBefore(endTime));
        }
    }

    /**
     *  if both not null, compare values;
     *  if any null, return 0;
     * @param d1 LocalDateTime
     * @param d2 LocalDateTime
     * @return the comparator value, negative if less, positive if greater
     */
    public static int compareTo(LocalDateTime d1, LocalDateTime d2) {
        if (d1 != null && d2 != null) {
            return d1.compareTo(d2);
        }
        return 0;
    }

    public static LocalDate getYearStart(LocalDate queryTime) {
        return queryTime.with(TemporalAdjusters.firstDayOfYear());
    }


    /**
     * Return the start date and end date of a week by input date, using WeekFields.ISO as default.
     * Eg: input: 2023-06-10, return: 2023-06-05 ~ 2023-06-11.
     *
     * @param date input local date
     */
    public static Tuple2<LocalDate, LocalDate> getWeekRange(LocalDate date) {
        Assert.notNull(date, "Input date is required.");
        return getWeekRange(date, WeekFields.ISO);
    }

    /**
     * Return the start date and end date of a week by input date.
     * Eg: input: 2023-06-10,
     * if WeekFields.ISO, return: [2023-06-05, 2023-06-11].
     * if WeekFields.SUNDAY_START, return: [2023-06-04, 2023-06-10].
     *
     * @param date input local
     * @param weekFields can be WeekFields.ISO or WeekFields.SUNDAY_START
     */
    public static Tuple2<LocalDate, LocalDate> getWeekRange(LocalDate date, WeekFields weekFields) {
        Assert.notNull(date, "Input date is required.");
        Assert.notNull(weekFields, "Input weekFields is required.");

        int currentWeekDay = date.get(weekFields.dayOfWeek());
        int startOfWeekInterval = (currentWeekDay - 1) * -1;
        int endOfWeekInterval = 7 - currentWeekDay;
        if (endOfWeekInterval < 0) {
            endOfWeekInterval = 0;
        }
        LocalDate startDate = date.plusDays(startOfWeekInterval);
        LocalDate endDate = date.plusDays(endOfWeekInterval);
        return Tuple.of(startDate, endDate);
    }
}
